package org.scs.carleton.ca.pgdt.core.ui.editors;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.part.MultiPageEditorPart;
import org.scs.carleton.ca.pgdt.core.ParseException;
import org.scs.carleton.ca.pgdt.core.PrologCore;
import org.scs.carleton.ca.pgdt.core.internal.parser.TokenMgrError;
import org.scs.carleton.ca.pgdt.core.parser.nodes.IASTProgram;
import org.scs.carleton.ca.pgdt.core.ui.Messages;
import org.scs.carleton.ca.pgdt.pdt.layer.IPrologSession;
import org.scs.carleton.ca.pgdt.pdt.layer.factory.PrologSessionFactory;
import org.scs.carleton.ca.pgdt.pdt.layer.wrapper.PrologQueryException;

/**
 * This is the evaluator aspect of the Prolog Editor. It depends on a prolog editor and therefore should not sit on its own.
 * 
 * @author tessierj
 */
public class EvaluatorEditor extends MultiPageEditorPart implements IResourceChangeListener {
	/* Used Strings */
	private static final String NO_RESULTS_AVAILABLE_UNDEFINED_PROCEDURE = "No results available; Undefined procedure (;)"; //$NON-NLS-1$
	private static final String UNABLE_TO_CONSULT_CONTENTS = "Unable to consult contents: "; //$NON-NLS-1$

	private static final String CLOSE_SESSION = Messages.getString("EvaluatorEditor.0"); //$NON-NLS-1$
	private static final String TRUE = Messages.getString("EvaluatorEditor.1"); //$NON-NLS-1$
	private static final String FALSE = Messages.getString("EvaluatorEditor.2"); //$NON-NLS-1$
	private static final String EVALUATOR = Messages.getString("EvaluatorEditor.3"); //$NON-NLS-1$
	private static final String SEND_TO_PROLOG = Messages.getString("EvaluatorEditor.4"); //$NON-NLS-1$
	private static final String UPDATE = Messages.getString("EvaluatorEditor.5"); //$NON-NLS-1$
	private static final String CLEAR_CONSOLE = Messages.getString("EvaluatorEditor.6"); //$NON-NLS-1$
	private static final String CONSULT_EDITOR_CONTENTS_LABEL = Messages.getString("EvaluatorEditor.7"); //$NON-NLS-1$
	private static final String ERROR_CREATING_NESTED_TEXT_EDITOR = Messages.getString("EvaluatorEditor.10"); //$NON-NLS-1$

	private static final String ASSIGNMENT = " = "; //$NON-NLS-1$
	private static final String END_OF_LINE = System.getProperty("line.separator"); //$NON-NLS-1$
	private static final String EMPTY_STRING = ""; //$NON-NLS-1$
	private static final String CONSULT_P2 = " ')"; //$NON-NLS-1$
	private static final String CONSULT_P1 = "consult('"; //$NON-NLS-1$
	private static final String USER_INPUT = " ?- "; //$NON-NLS-1$
	private static final String SEMICOLON = ";";//$NON-NLS-1$

	// Text Components
	private Text codeView;
	private Text prologInput;
	private Text console;

	// Buttons
	private Button sendToPrologButton;
	private Button clearConsoleButton;
	private Button consultButton;
	private Button closeSessionButton;

	// Editors
	private TextEditor prologEditor;

	// Listeners
	private SelectionListener sendToPrologListener, consultEditorListener, sashListener, closeListener, clearConsoleButtonListener;

	// Other
	private Sash sash;
	private Composite container, footer, header;

	/**
	 * The file path
	 */
	private String pathToEditorContents;

	/**
	 * The prolog session
	 */
	private IPrologSession session;
	private Label codeViewLabel;
	private List<Map<String, Object>> lastResults;
	private Iterator<Map<String, Object>> lastResultsIterator;

	/**
	 * Creates a multi-page editor example.
	 */
	public EvaluatorEditor() {
		super();
		lastResults = Collections.emptyList();
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
	}

	/**
	 * Add the listeners to the objects.
	 */
	private void addListeners() {
		if (sendToPrologButton != null) {
			// Create the send to prolog listener if needed
			sendToPrologListener = new SelectionListener() {
				public void widgetSelected(SelectionEvent event) {
					handlePrologInput();
				}

				public void widgetDefaultSelected(SelectionEvent event) {
					handlePrologInput();
				}

				private void handlePrologInput() {
					if (!(session == null || prologInput == null)) {
						String input = prologInput.getText();
						if (input != null) {
							input = input.trim();

							console.append(USER_INPUT + input + END_OF_LINE);

							// We want to accept semicolons as well.
							// If we have a semicolon, we do not need to query the session; we have the result already.
							if (input.equals(SEMICOLON)) {
								if (lastResultsIterator != null && lastResultsIterator.hasNext()) {
									// Get the next result
									handleResult(lastResultsIterator.next());
									update();
								} else {
									// If there are no results, inform the user
									console.append(NO_RESULTS_AVAILABLE_UNDEFINED_PROCEDURE + END_OF_LINE);
								}
							} else if (!input.equals(EMPTY_STRING)) {
								try {
									// If the query exists, send it to prolog and display the reuslts
									lastResults = session.queryAll(input);
									lastResultsIterator = lastResults.iterator();

									if (lastResultsIterator.hasNext()) {
										handleResult(lastResultsIterator.next());
										update();
									} else {
										handleResult(null);
									}
								} catch (Exception e) {
									console.append(e.getMessage() + END_OF_LINE);
								}
							}
						}
					}
				}
			};

			sendToPrologButton.addSelectionListener(sendToPrologListener);
		}

		if (sash != null) {
			sashListener = new SelectionAdapter() {
				public void widgetSelected(SelectionEvent event) {
					int totalHeight = sash.getParent().getBounds().height;
					((GridData) codeView.getLayoutData()).heightHint = event.y - 2 * sash.getBounds().height;
					((GridData) console.getLayoutData()).heightHint = totalHeight - event.y + 2 * sash.getBounds().height;
					sash.getParent().layout();
				}
			};

			sash.addSelectionListener(sashListener);
		}

		if (consultButton != null) {
			consultEditorListener = new SelectionListener() {
				public void widgetSelected(SelectionEvent event) {
					createSession();
				}

				public void widgetDefaultSelected(SelectionEvent event) {
					createSession();
				}

				private void createSession() {
					if (!(prologEditor == null || codeView == null)) {
						IASTProgram program;
						IEditorInput input = prologEditor.getEditorInput();
						if (input instanceof IFileEditorInput) {
							try {
								pathToEditorContents = ((IFileEditorInput) input).getFile().getLocation().toPortableString();
								program = PrologCore.getDefault().getASTProvider().getProgram(((IFileEditorInput) input).getFile());
								codeView.setText(program.getTranslationUnit().getSource());
							} catch (ParseException e) {
								console.append(UNABLE_TO_CONSULT_CONTENTS + e.getMessage() + END_OF_LINE);
								// No need to do anything, we don't have a program
							} catch (TokenMgrError e) {
								console.append(UNABLE_TO_CONSULT_CONTENTS + e.getMessage() + END_OF_LINE);
								// No need to do anything, we don't have a program
							}
						}

						console.append(USER_INPUT + CONSULT_P1 + pathToEditorContents + CONSULT_P2 + END_OF_LINE);
						session = createPrologSession();

						update();
					}
				}
			};

			consultButton.addSelectionListener(consultEditorListener);
		}

		if (clearConsoleButton != null) {
			clearConsoleButtonListener = new SelectionListener() {
				public void widgetSelected(SelectionEvent event) {
					console.setText(EMPTY_STRING);
					update();
				}

				public void widgetDefaultSelected(SelectionEvent event) {
					console.setText(EMPTY_STRING);
					update();
				}
			};

			clearConsoleButton.addSelectionListener(clearConsoleButtonListener);
		}

		if (!(closeSessionButton == null)) {
			if (closeListener == null) {
				closeListener = new SelectionListener() {
					public void widgetSelected(SelectionEvent event) {
						session = null;
						update();
					}

					public void widgetDefaultSelected(SelectionEvent event) {
						session = null;
						update();
					}
				};
			}

			closeSessionButton.addSelectionListener(closeListener);
		}
	}

	private void update() {
		boolean sessionExists = (session != null);
		// First, adjust the text elements
		console.setEnabled(sessionExists);
		console.setEditable(false);

		codeView.setEnabled(sessionExists);
		codeView.setEditable(false);

		prologInput.setEnabled(sessionExists);
		prologInput.setEditable(sessionExists);

		// Adjust the buttons
		consultButton.setEnabled(true); // Always enabled
		consultButton.setText(sessionExists ? UPDATE : CONSULT_EDITOR_CONTENTS_LABEL);
		sendToPrologButton.setEnabled(sessionExists);
		clearConsoleButton.setEnabled(sessionExists);
		closeSessionButton.setEnabled(sessionExists);
	}

	/**
	 * Creates page 0 of the multi-page editor, which contains a text editor.
	 */
	void createPage0() {
		// Page 0 is the Prolog Editor
		try {
			prologEditor = new PrologEditor();
			setPageText(addPage(prologEditor, getEditorInput()), prologEditor.getTitle());
		} catch (PartInitException e) {
			ErrorDialog.openError(getSite().getShell(), ERROR_CREATING_NESTED_TEXT_EDITOR, null, e.getStatus());
		}
	}

	/**
	 * Creates page 1 of the multi-page editor, which allows you to change the font used in page 2.
	 */
	void createPage1() {
		// Page 1 is the Prolog Evaluator system
		// Create the major components of the editor
		container = new Composite(getContainer(), SWT.NONE);
		header = new Composite(container, SWT.NONE);

		// Create the secondary components of the editor
		codeView = new Text(container, SWT.BORDER | SWT.WRAP | SWT.V_SCROLL | SWT.H_SCROLL);
		sash = new Sash(container, SWT.HORIZONTAL);
		console = new Text(container, SWT.MULTI | SWT.V_SCROLL | SWT.BORDER);

		footer = new Composite(container, SWT.NONE);

		// Add layout information
		addLayout(container);
		fillHeader(header);
		fillFooter(footer);
		addLayoutInformation();
		addListeners();
		update();

		int index = addPage(container);
		setPageText(index, EVALUATOR);
	}

	/**
	 * Adds a layour manager to the composite
	 * 
	 * @param container
	 */
	private void addLayout(Composite container) {
		GridLayout layout = new GridLayout();
		layout.marginWidth = 2;
		layout.marginHeight = 2;
		layout.horizontalSpacing = 0;
		layout.verticalSpacing = 2;
		container.setLayout(layout);
	}

	/**
	 * Adds composite information and layout information for the footer
	 * 
	 * @param footer
	 */
	private void fillFooter(Composite footer) {
		// First, add a layout
		footer.setLayout(new RowLayout());

		// The footer contains a text box for input and a button to send it ot the console. By default, they are disabled.
		prologInput = new Text(footer, SWT.BORDER);
		RowData data = new RowData();
		data.width = 300;
		prologInput.setLayoutData(data);

		sendToPrologButton = new Button(footer, SWT.PUSH);
		sendToPrologButton.setText(SEND_TO_PROLOG);
		sendToPrologButton.setSize(sendToPrologButton.computeSize(SWT.DEFAULT, SWT.DEFAULT));

		clearConsoleButton = new Button(footer, SWT.PUSH);
		clearConsoleButton.setText(CLEAR_CONSOLE);
		clearConsoleButton.setSize(clearConsoleButton.computeSize(SWT.DEFAULT, SWT.DEFAULT));
	}

	/**
	 * Adds composites and layout information for the header
	 * 
	 * @param header
	 */
	private void fillHeader(Composite header) {
		// First, add a layout
		header.setLayout(new GridLayout(2, false));

		GridData data = new GridData();
		consultButton = new Button(header, SWT.PUSH);
		consultButton.setText(CONSULT_EDITOR_CONTENTS_LABEL);
		consultButton.setSize(consultButton.computeSize(SWT.DEFAULT, SWT.DEFAULT));
		consultButton.setLayoutData(data);

		data = new GridData();
		closeSessionButton = new Button(header, SWT.PUSH);
		closeSessionButton.setText(CLOSE_SESSION);
		closeSessionButton.setSize(closeSessionButton.computeSize(SWT.DEFAULT, SWT.DEFAULT));
		closeSessionButton.setLayoutData(data);

		data = new GridData();
		data.horizontalSpan = 2;
		codeViewLabel = new Label(header, SWT.NONE);
		codeViewLabel.setText(Messages.getString("EvaluatorEditor.15")); //$NON-NLS-1$
		codeViewLabel.setSize(codeViewLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT));
		codeViewLabel.setLayoutData(data);
	}

	/**
	 * Creates the pages of the multi-page editor.
	 */
	protected void createPages() {
		createPage0();
		createPage1();
		setPartName(prologEditor.getTitle());
	}

	/**
	 * The <code>MultiPageEditorPart</code> implementation of this <code>IWorkbenchPart</code> method disposes all nested editors. Subclasses may extend.
	 */
	public void dispose() {
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
		// Make sure to end the session... otherwise a prolog process stays active.
		if (session != null) {
			session.endSession();
		}
		// removeListeners(); Widgets dispose themselves
		super.dispose();
	}

	/**
	 * Saves the multi-page editor's document.
	 */
	public void doSave(IProgressMonitor monitor) {
		getEditor(0).doSave(monitor);
	}

	/**
	 * Saves the multi-page editor's document as another file. Also updates the text for page 0's tab, and updates this multi-page editor's input to correspond to the nested editor's.
	 */
	public void doSaveAs() {
		IEditorPart editor = getEditor(0);
		editor.doSaveAs();
		setPageText(0, editor.getTitle());
		setInput(editor.getEditorInput());
	}

	/*
	 * (non-Javadoc) Method declared on IEditorPart
	 */
	public void gotoMarker(IMarker marker) {
		setActivePage(0);
		IDE.gotoMarker(getEditor(0), marker);
	}

	/**
	 * The <code>MultiPageEditorExample</code> implementation of this method checks that the input is an instance of <code>IFileEditorInput</code>.
	 */
	public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
		if (!(editorInput instanceof IFileEditorInput))
			throw new PartInitException("Invalid Input: Must be IFileEditorInput"); //$NON-NLS-1$
		super.init(site, editorInput);
	}

	/*
	 * (non-Javadoc) Method declared on IEditorPart.
	 */
	public boolean isSaveAsAllowed() {
		return true;
	}

	/**
	 * Calculates the contents of page 2 when the it is activated.
	 */
	protected void pageChange(int newPageIndex) {
		super.pageChange(newPageIndex);
	}

	/**
	 * Closes all project files on project close.
	 */
	public void resourceChanged(final IResourceChangeEvent event) {
		if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
			Display.getDefault().asyncExec(new Runnable() {
				public void run() {
					IWorkbenchPage[] pages = getSite().getWorkbenchWindow().getPages();
					for (int i = 0; i < pages.length; i++) {
						if (((FileEditorInput) prologEditor.getEditorInput()).getFile().getProject().equals(event.getResource())) {
							IEditorPart editorPart = pages[i].findEditor(prologEditor.getEditorInput());
							pages[i].closeEditor(editorPart, true);
						}
					}
				}
			});
		}
	}

	/**
	 * Adds layout information to the components
	 */
	private void addLayoutInformation() {
		// Add the grid data
		GridData data = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL);
		data.grabExcessVerticalSpace = true;
		data.minimumHeight = 50;
		codeView.setLayoutData(data);

		data = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_END);
		header.setLayoutData(data);

		data = new GridData(GridData.FILL_HORIZONTAL);
		data.heightHint = 3;
		data.verticalIndent = 0;
		sash.setLayoutData(data);

		data = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL);
		data.grabExcessVerticalSpace = true;
		data.minimumHeight = 50;
		console.setLayoutData(data);

		data = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_END);
		footer.setLayoutData(data);
	}

	/**
	 * Creates a prolog session based on the file name.
	 * 
	 * TODO: Fix this so that there is no conflict between multiple files with the same name.
	 * 
	 * @return
	 */
	private IPrologSession createPrologSession() {
		if (pathToEditorContents != null) {
			try {
				IPrologSession session = PrologSessionFactory.createSession(this.getPartName());
				session.queryOnce(CONSULT_P1 + pathToEditorContents + "')" + END_OF_LINE); //$NON-NLS-1$
				return session;
			}
			catch (PrologQueryException e) {
				console.append(UNABLE_TO_CONSULT_CONTENTS + e.getMessage() + END_OF_LINE);
			}
		}

		return null;
	}

	/**
	 * This will append information to the console given a query result. This is not the entire set of results, but only a single result.
	 * 
	 * (Aka, this is equivalent to pressing ; assuming a query has already been run).
	 * 
	 * @param queryResult
	 */
	private void handleResult(Map<String, Object> queryResult) {
		if (queryResult == null) {
			// If the result is empty, display false (weird quirk of PDT)
			console.append(FALSE);
		} else if (queryResult.isEmpty()) {
			// If the result returned true with no bindings, display true
			console.append(TRUE);
		} else {
			// Otherwise, filter through all of the bindings and display them
			Set<Entry<String, Object>> entries = queryResult.entrySet();
			StringBuffer buffer;
			for (Entry<String, Object> entry : entries) {
				buffer = new StringBuffer();
				buffer.append(entry.getKey());
				buffer.append(ASSIGNMENT);
				buffer.append(entry.getValue());
				buffer.append(END_OF_LINE);
				console.append(buffer.toString());
			}
		}
	}
}
